package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheUtils;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.util.SystemPropertyUtils;

import javax.annotation.Resource;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Resource
    CacheUtils cacheUtils;
    private final static ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    @Override
    public Result select(Long id) {
        //缓存穿透和缓存击穿不要用一个key存，逻辑过期存的是redisData,穿透直接存shop。
        //缓存穿透
        // Result result = queryWithCacheThrough(id);
        Shop shop1 = cacheUtils.queryWithPassThrough(CACHEPassThrough_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //缓存击穿:互斥锁解决
        //Shop shop = queryWithMutex(id);

        //缓存击穿:逻辑过期解决
        //Shop shop = queryWhitExpireLock(id);
        Shop shop2 = cacheUtils.queryWhitExpireLock(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        if (shop1 == null) {
            //返回来的是null，就是数据库中没有这个商户。
            return Result.fail("没有商铺信息");
        }
        return Result.ok(shop2);
    }

    /**
     * 使用互斥锁，解决缓存击穿，热门数据丢失
     */
    public Shop queryWithMutex(Long id) {
        //先查缓存
        String JsonShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        if (!StringUtils.isEmpty(JsonShop)) {//查到了直接返回
            return JSONUtil.toBean(JsonShop, Shop.class);
        }
        if (JsonShop != null) {//查到了""，这个表示数据库现在没有数据，防止缓存穿透不要再去数据库查了，直接返回
            return null;
        }
        //查到null,表示缓存没有数据请查数据库：
        //1获取锁，对缓存重建，加id是一个商铺一个锁
        String key = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(key);
        Shop shop = null;
        try {
            if (!isLock) {
                //没拿到锁说明别的线程已经再重建了。过50毫秒再重试
                Thread.sleep(50);
                return queryWithMutex(id);//重试的时候别的线程可能已经将数据写到缓存了，就不会再查数据库
            }
            Thread.sleep(200);//这里模拟高并发。
            //2拿到锁了，将数据库中的数据重写到缓存
            shop = query().eq("id", id).one();
            if (ObjectUtil.isNull(shop)) {
                //缓存一个空值，预防缓存穿透。
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //写回缓存.设置超时时间，防止缓存更新失败，作为兜底。
            JsonShop = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JsonShop, CACHE_SHOP_TTL, TimeUnit.MINUTES);
            //3释放锁
        } catch (Exception e) {
            throw new RuntimeException("重建缓存异常");
        } finally {
            unLock(key);
        }
        return shop;
    }

    /**
     * 使用逻辑过期，解决缓存击穿，热门数据丢失
     */
    public Shop queryWhitExpireLock(Long id) {
        //1.先查缓存
        String redisResult = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        if (StringUtils.isEmpty(redisResult)) {
            return null;//一般后台系统会先将热门数据先加到缓存中，然后设置逻辑过期(实质是永不过期的，所以一定能查到数据，查不到要么redis挂了，要么不存在这个商铺)。这边用单元测试先预热一下数据
        }
        //1.2拿到数据先看看没有有过期，没过期直接返回，过期了重建
        RedisData redisData = JSONUtil.toBean(redisResult, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        if (expireTime.isAfter(LocalDateTime.now())) {
            System.out.println("没过期");
            return shop;
        }
        //2.过期了重建数据
        //2.1获取锁
        String key = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(key);
        //2.2拿到锁了，二次检查缓存中有没有数据，是否过期。没有再新建线程去重建缓存
        if (isLock) {
                String redisResultTemp = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
                if (!StringUtils.isEmpty(redisResultTemp)) {
                    RedisData redisDataTemp = JSONUtil.toBean(redisResult, RedisData.class);
                    LocalDateTime expireTemp = redisDataTemp.getExpireTime();
                    if (!expireTemp.isAfter(LocalDateTime.now())) {
                    //二次确认过期了，开启新线程去重建数据
                        CACHE_REBUILD_EXECUTOR.submit(() -> {
                            try {
                                System.out.println("重建数据");
                            saveRedisHotData(id, 20L);
                            } catch (Exception e) {
                                throw new RuntimeException("重建数据异常");
                            } finally {
        //3.释放锁
                                unLock(key);
                                System.out.println("重建完成");
                            }
                        });
                    }
                }

        }
    //4.没拿到锁先返回过期的数据
        System.out.println("没拿到锁返回旧数据");
        return shop;
    }

    /**
     * 解决缓存穿透
     */
    public Result queryWithCacheThrough(Long id) {
        //先查缓存
        String JsonShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        if (!StringUtils.isEmpty(JsonShop)) {
            Shop shopCache = JSONUtil.toBean(JsonShop, Shop.class);
            return Result.ok(shopCache);
        }
        //情况1：缓存查不到，则是null,这种就是没有缓存，去查数据库
        //情况2：缓存查到了，但是值为"",这种就是数据库暂时没有数据，直接返回提示，这个缓存会设置一个短的有效期。
        if (JsonShop != null) {
            //预防缓存穿透，如果里面是""，则是有一个查询个查了一次数据库，但是数据库里没有数据
            //数据库没有数据，防止用户再次查询，又走数据库增大数据库压力。所以让缓存命中
            return Result.fail("数据库暂时没有数据，请两分钟后再刷新试试");
        }
        //缓存没有再查数据库
        Shop shop = query().eq("id", id).one();
        if (ObjectUtil.isNull(shop)) {
            //缓存一个空值，预防缓存穿透。
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("数据库暂时没有数据，请两分钟后再刷新试试");
        }
        //写回缓存.设置超时时间，防止缓存更新失败，作为兜底。
        JsonShop = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JsonShop, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

    @Override
    @Transactional//保证这个方法的原子性
    public Result updateShop(Shop shop) {
        //两个操作需要保证原子性，加上事务注解
        if (shop.getId() == null) {
            return Result.fail("商户id为空");
        }
        //1先更新数据库
        updateById(shop);
        //2删除缓存,如果删除失败出现异常，则数据库更新也会回滚
        stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());
        return Result.ok();
    }

    boolean tryLock(String key) {
        //防止线程异常锁没有被释放，给一个过期时间
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(isLock);//直接返回包装类拆箱时可能出现NPE
    }

    void unLock(String key) {
        Boolean isLock = stringRedisTemplate.delete(key);
    }

    public void saveRedisHotData(long id, long expireSeconds) throws InterruptedException {
        //从数据库中取出数据，给数据加一个逻辑过期时间后存入redis
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }
}

